﻿//-----------------------------------------------------------------------------------------------------------
// Wbfs Engine (WBFSe) Project by Omega Frost 
// http://wbfse.codeplex.com/
//
// WBFSe is Licensed under the terms of the 
// Microsoft Reciprocal License (Ms-RL)
//-----------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using WBFSe.Attributes;
using WBFSe.IO.Streams;
using WBFSe.OS;
using System.Threading;
using System.IO;
using System.Runtime.InteropServices;
using WBFSe.Data;

namespace WBFSe.IO.Drives
{
    /// <summary>
    /// A WBFS Formatted device
    /// </summary>
    class WbfsDrive : IWbfsDriveInfo
    {
        // Variables

        String name;
        String label;
        DriveType type;

        long size;
        
        int totalHdSectors;
        int totalWbfsSectors;
        int usedWbfsSectors;

        int wbfsSectorSize;
        byte wbfsSectorSizeSz;

        int wbfsWiiRatio;
        int wbfsSectorsPerDisc;

        int hdSectorSize;
        byte hdSectorSizeSz;

        Byte[] discTable;
        Byte[] sectorTable;

        int sectorTablePosition;
        int sectorTableSize;

        int discDescriptorSize;
        int maxDiscs;

        List<FileSystemEntry> fileTable;

        SemaphoreSlim semaphore;
        IWbfsStream stream;
        UnmanagedArray buffer;

        // Properties

        /// <summary>
        /// The drive name in format X:\
        /// </summary>
        public String Name
        {
            get { return this.name; }
        }

        /// <summary>
        /// The drive label
        /// </summary>
        public String Label
        {
            get { return this.label; }
            set { }
        }

        /// <summary>
        /// The drive FS
        /// </summary>
        public String FileSystem
        {
            get { return "WBFS"; }
        }

        /// <summary>
        /// The drive type
        /// </summary>
        public DriveType Type
        {
            get { return this.type; }
        }

        /// <summary>
        /// The partition is WBFS
        /// </summary>
        public bool IsWbfs
        {
            get { return true; }
        }

        /// <summary>
        /// The drive is marked as a WbfsFat
        /// </summary>
        public bool IsWbfsFat
        {
            get { return false; }
            set { }
        }

        /// <summary>
        /// The drive is accessible
        /// </summary>
        public bool IsReady
        {
            get { return IOTest(); }
        }

        /// <summary>
        /// The file system supports sparse file
        /// </summary>
        public bool IsSparse
        {
            get { return true; }
        }

        /// <summary>
        /// The size of the drive
        /// </summary>
        public long Size
        {
            get { return (long)this.totalWbfsSectors * this.wbfsSectorSize; }
        }

        /// <summary>
        /// The ammount of free user space
        /// </summary>
        public long Free
        {
            get { return this.size - this.Used; }
        }

        /// <summary>
        /// The ammount of used user space
        /// </summary>
        public long Used
        {
            get { return (long)this.usedWbfsSectors * this.wbfsSectorSize; }
        }

        /// <summary>
        /// The physical sector size
        /// </summary>
        public int HdSectorSize
        {
            get { return this.hdSectorSize; }
        }

        /// <summary>
        /// The file system sector size
        /// </summary>
        public int WbfsSectorSize
        {
            get { return this.wbfsSectorSize; }
        }

        /// <summary>
        /// The maximum length of the discs stored in the partition
        /// </summary>
        public long DiscLength
        {
            get { return (long)this.wbfsSectorsPerDisc << this.wbfsSectorSizeSz; }
        }

        // Methods

        /// <summary>
        /// Formats a drive as wbfs
        /// </summary>
        /// <param name="di">Drive info</param>
        /// <param name="drive">Out drive</param>
        /// <returns>False if the drive was not found</returns>
        /// <exception cref="System.Exception">On operation failure</exception>
        internal static bool Format(DriveInfo di, out IWbfsDriveInfo drive)
        {
            if (di.DriveType == DriveType.NoRootDirectory)
            {
                drive = null;
                return false;
            }

            drive = new WbfsDrive(di, true);
            return true;
        }

        /// <summary>
        /// Opens a wbfs drive
        /// </summary>
        /// <param name="di">Drive info</param>
        /// <param name="drive">Out drive</param>
        /// <returns>False if drive is not wbfs</returns>
        /// <exception cref="System.Exception">On operation failure</exception>
        internal static bool Open(DriveInfo di, out IWbfsDriveInfo drive)
        {
            String name = di.Name.ToUpper();

            Byte[] bbuffer = null;
            UnmanagedArray buffer = null;
            WbfsFileStream stream = new WbfsFileStream(@"\\.\" + name[0] + ':',
                System.IO.FileMode.Open, System.IO.FileAccess.ReadWrite,
                System.IO.FileShare.ReadWrite);

            try
            {
                int sectorSize;
                if (!Native.GetSectorSize(stream.FilePtr, out sectorSize))
                    throw new WbfsIOException(Marshal.GetLastWin32Error());

                buffer = new UnmanagedArray(sectorSize);
                stream.Read(buffer.DataPtr, 0, 0, sectorSize);
                stream.Close();

                bbuffer = new Byte[4];
                buffer.CopyTo(bbuffer, 0, 4);
                buffer.Dispose();
            }
            catch
            {
                if (buffer != null)
                    buffer.Dispose();
                stream.Close();
                throw;
            }

            if (Ntoh.ReadUInt32(bbuffer, 0) != WbfsConstantsV1.WbfsMagic)
            {
                drive = null;
                return false;
            }

            drive = new WbfsDrive(di, false);
            return true;
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="info">DriveInfo</param>
        private WbfsDrive(DriveInfo info, bool format)
        {
            this.name = info.Name.ToUpper();
            this.type = (DriveType)info.DriveType;
            this.label = String.Empty;

            this.semaphore = new SemaphoreSlim(1);

            OpenDrive();

            if (!format)
            {
                ParseDisk();
                ParseHeader();
                ParseSectorTable();
                ParseGameTable();
            }
            else
            {
                if (!Native.LockVolume(this.stream.FilePtr))
                    throw new WbfsIOException(Marshal.GetLastWin32Error());

                ParseDisk();
                MakeHeader();
                ParseHeader();
                MakeSectorTable();
                ParseSectorTable();
                ParseGameTable();

                if (!Native.UnlockVolume(this.stream.FilePtr))
                    throw new WbfsIOException(Marshal.GetLastWin32Error());
            }
            
            CloseDrive();
        }

        /// <summary>
        /// Updates the drive
        /// </summary>
        public void Update()
        {
        }

        /// <summary>
        /// Opens the device stream for FS operation
        /// </summary>
        private void OpenDrive()
        {
            this.stream = new WbfsFileStream(@"\\.\" + this.name[0] + ':',
                System.IO.FileMode.Open, System.IO.FileAccess.ReadWrite,
                System.IO.FileShare.ReadWrite);
        }

        /// <summary>
        /// Closes the device stream
        /// </summary>
        private void CloseDrive()
        {
            this.stream.Close();
            this.stream = null;
        }

        /// <summary>
        /// Closes the drive
        /// </summary>
        public void Close()
        {
        }

        /// <summary>
        /// Tests if the drive is responding
        /// </summary>
        /// <returns></returns>
        private bool IOTest()
        {
            try
            {
                IWbfsStream stream = new WbfsFileStream(@"\\.\" + this.name[0] + ':',
                System.IO.FileMode.Open, System.IO.FileAccess.ReadWrite,
                System.IO.FileShare.ReadWrite);

                stream.Close();
            }
            catch
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Enumerates files in a directory of the drive
        /// </summary>
        /// <param name="path">The full path</param>
        /// <returns>The filenames</returns>
        /// <exception cref="System.ArgumentException">The path is invalid or the root mismath</exception>
        /// <exception cref="System.Exception">On operation failure</exception>
        public String[] GetFilenames(String path)
        {
            path = System.IO.Path.GetFullPath(path);
            if (Char.ToUpper(path[0]) != Char.ToUpper(this.Name[0]))
                throw new ArgumentException("The path does not belong to the drive");

            semaphore.Wait();

            String[] files = new String[this.fileTable.Count];

            for (int i = 0; i < this.fileTable.Count; i++)
                files[i] = this.fileTable[i].ToString();

            semaphore.Release();
            return files;
        }

        /// <summary>
        /// Parses the disk attributes and creates the main buffer
        /// </summary>
        private void ParseDisk()
        {
            if(!Native.GetSectorSize(this.stream.FilePtr, out this.hdSectorSize))
                throw new WbfsIOException(Marshal.GetLastWin32Error());

            if(!Native.GetPartitionSize(stream.FilePtr, out this.size))
                throw new WbfsIOException(Marshal.GetLastWin32Error());

            this.hdSectorSizeSz = Lsh(this.hdSectorSize);
            this.totalHdSectors = (int)(this.size >> this.hdSectorSizeSz);

            this.wbfsSectorSizeSz = WbfsSectorSizeSz();
            this.wbfsSectorSize = 1 << this.wbfsSectorSizeSz;
            this.totalWbfsSectors = (int)(this.size >> this.wbfsSectorSizeSz);

            this.buffer = new UnmanagedArray(this.wbfsSectorSize);
        }

        /// <summary>
        /// Parses de file system header
        /// </summary>
        private void ParseHeader()
        {
            this.stream.Read(this.buffer.DataPtr, 0, 0, this.hdSectorSize);

            Byte[] head = new Byte[this.hdSectorSize];
            this.buffer.CopyTo(head, 0, this.hdSectorSize);

            if (Ntoh.ReadUInt32(head, WbfsConstantsV1.WbfsHeadMagicPos) != WbfsConstantsV1.WbfsMagic)
                throw new FormatException("Wbfs partition is corrupted, invalid code");

            if (Ntoh.ReadInt32(head, WbfsConstantsV1.WbfsHeadNSecsPos) != this.totalHdSectors)
                throw new FormatException("Wbfs partition is corrupted, invalid size");

            if(head[WbfsConstantsV1.WbfsHeadHdSecSzPos] != this.hdSectorSizeSz)
                throw new FormatException("Wbfs partition is corrupted, invalid hd sector size");

            if(head[WbfsConstantsV1.WbfsHeadWbfsSecSzPos] != this.wbfsSectorSizeSz)
                throw new FormatException("Wbfs partition is corrupted, invalid wbfs sector size");

            this.wbfsWiiRatio = this.wbfsSectorSizeSz - WbfsConstantsV1.WiiSectorSizeLSh;
            this.wbfsSectorsPerDisc = WbfsConstantsV1.WiiSectorsPerDisc >> this.wbfsWiiRatio;
            this.wbfsWiiRatio = 1 << this.wbfsWiiRatio;

            int sectblbytes = ((this.totalWbfsSectors - 1) >> 3) + 1;
            int sectblsecs = ((sectblbytes - 1) >> this.hdSectorSizeSz) + 1;
            
            this.sectorTableSize = sectblsecs << this.hdSectorSizeSz;
            this.sectorTablePosition = this.wbfsSectorSize - this.sectorTableSize;

            int discdescbytes = WbfsConstantsV1.WbfsDiscHCopySz + 
                sizeof(short) * this.wbfsSectorsPerDisc;
            int discdescsecs = ((discdescbytes - 1) >> this.hdSectorSizeSz) + 1;

            this.discDescriptorSize = discdescsecs << this.hdSectorSizeSz;

            this.maxDiscs = Math.Min((this.sectorTablePosition - this.hdSectorSize) /
                this.discDescriptorSize, this.hdSectorSize - WbfsConstantsV1.WbfsHeadDiscTablePos);

            this.discTable = new Byte[this.maxDiscs];
            this.sectorTable = new Byte[this.discDescriptorSize];

            Array.Copy(head, WbfsConstantsV1.WbfsHeadDiscTablePos, 
                this.discTable, 0, this.maxDiscs);
        }

        /// <summary>
        /// Parses de file system sector table
        /// </summary>
        private void ParseSectorTable()
        {
            this.stream.Read(this.buffer.DataPtr, 0, this.sectorTablePosition, 
                this.sectorTableSize);

            this.buffer.CopyTo(this.sectorTable, 0, this.sectorTableSize);

            for (int i = 0; i < this.sectorTableSize; i += 4)
            {
                byte temp;
                
                temp = this.sectorTable[i];
                this.sectorTable[i] = this.sectorTable[i + 3];
                this.sectorTable[i + 3] = temp;

                temp = this.sectorTable[i + 1];
                this.sectorTable[i + 1] = this.sectorTable[i + 2];
                this.sectorTable[i + 2] = temp;
            }

            this.usedWbfsSectors = 0;

            int ibyte = 0, ibit = 0;
            for (int i = 0; i < this.totalWbfsSectors; i++)
            {
                this.usedWbfsSectors += (~(this.sectorTable[ibyte] >> ibit) & 0x1);
                if (++ibit == 8)
                {
                    ibit = 0;
                    ibyte++;
                }
            }
        }

        /// <summary>
        /// Parses the file system game table
        /// </summary>
        private void ParseGameTable()
        {
            this.fileTable = new List<FileSystemEntry>();

            Byte[] header = new Byte[this.hdSectorSize];
            FileSystemEntry fse;

            for (int i = 0; i < this.maxDiscs; i++)
            {
                if (this.discTable[i] == 0)
                    continue;

                this.stream.Read(this.buffer.DataPtr, 0, this.hdSectorSize +
                    this.discDescriptorSize * i, this.hdSectorSize);

                this.buffer.CopyTo(header, 0, this.hdSectorSize);

                fse = new FileSystemEntry(i, 
                    ReadString(header, WbfsConstantsV1.WiiNamePos, WbfsConstantsV1.WiiNameLen),
                    ReadString(header, WbfsConstantsV1.WiiCodePos, WbfsConstantsV1.WiiCodeLen));

                int p = this.fileTable.BinarySearch(fse, fse);
                if (p >= 0) throw new IOException("File already exists in the table");

                this.fileTable.Insert(~p, fse);
            }
        }

        /// <summary>
        /// Creates a new header for the partition
        /// </summary>
        private void MakeHeader()
        {
            Byte[] head = new Byte[this.hdSectorSize];
            Array.Clear(head, 0, this.hdSectorSize);

            Hton.WriteUInt32(head, WbfsConstantsV1.WbfsHeadMagicPos, WbfsConstantsV1.WbfsMagic);
            Hton.WriteInt32(head, WbfsConstantsV1.WbfsHeadNSecsPos, this.totalHdSectors);
            head[WbfsConstantsV1.WbfsHeadHdSecSzPos] = this.hdSectorSizeSz;
            head[WbfsConstantsV1.WbfsHeadWbfsSecSzPos] = this.wbfsSectorSizeSz;

            this.buffer.CopyFrom(head, 0, 0, this.hdSectorSize);
            this.stream.Write(this.buffer.DataPtr, 0, 0, this.hdSectorSize);
        }

        /// <summary>
        /// Creates a new sector table for the partition
        /// </summary>
        private void MakeSectorTable()
        {
            Byte[] sectbl = new Byte[this.sectorTableSize];

            for (int i = 0; i < this.sectorTableSize; i++)
                sectbl[i] = 0xFF;

            this.buffer.CopyFrom(sectbl, 0, 0, this.sectorTableSize);

            this.stream.Write(this.buffer.DataPtr, 0, 
                this.sectorTablePosition, this.sectorTableSize);
        }

        /// <summary>
        /// Reads the disc's description
        /// </summary>
        /// <param name="filename">File name</param>
        /// <param name="header">Out disc header</param>
        /// <param name="sectors">Out sectors</param>
        /// <returns>False if the file does not exists</returns>
        internal bool QueryDiscDesc(string filename, out byte[] header, out ushort[] sectors)
        {
            semaphore.Wait();

            Byte[] desc = new Byte[this.discDescriptorSize];

            FileSystemEntry entry = new FileSystemEntry(0, 
                filename.Substring(7), filename.Substring(0, 6));

            int p = this.fileTable.BinarySearch(entry, entry);
            if (p < 0)
            {
                sectors = null;
                header = null;
                return false;
            }

            int discIdx = this.fileTable[p].DiscIndex;

            try
            {
                OpenDrive();

                this.stream.Read(this.buffer.DataPtr, 0, this.hdSectorSize +
                    discIdx * this.discDescriptorSize, this.discDescriptorSize);

                this.buffer.CopyTo(desc, 0, this.discDescriptorSize);

                header = new Byte[WbfsConstantsV1.WbfsDiscHCopySz];
                sectors = new ushort[this.wbfsSectorsPerDisc];

                Array.Copy(desc, 0, header, 0, WbfsConstantsV1.WbfsDiscHCopySz);

                for (int i = 0; i < this.wbfsSectorsPerDisc; i++)
                    sectors[i] = Ntoh.ReadUInt16(desc, sizeof(ushort) * i +
                        WbfsConstantsV1.WbfsDiscHCopySz);

                CloseDrive();
                semaphore.Release();

                return true;
            }
            catch
            {
                CloseDrive();
                semaphore.Release();

                throw;
            }
        }

        /// <summary>
        /// Sets the disc's description
        /// </summary>
        /// <param name="filename">File name</param>
        /// <param name="header">Optional new disc header</param>
        /// <param name="sectors">Optional new disc sectors</param>
        /// <returns>False if the file does not exists</returns>
        internal bool SetDiscDesc(string filename, byte[] header, ushort[] sectors)
        {
            semaphore.Wait();

            FileSystemEntry entry = new FileSystemEntry(0,
                filename.Substring(7), filename.Substring(0, 6));

            int p = this.fileTable.BinarySearch(entry, entry);
            if (p < 0) return false;

            int discIdx = this.fileTable[p].DiscIndex;

            try
            {
                OpenDrive();

                this.stream.Read(this.buffer.DataPtr, 0, this.hdSectorSize +
                        this.discDescriptorSize * discIdx, this.discDescriptorSize);

                if (header != null)
                {
                    this.buffer.CopyFrom(header, 0, 0, WbfsConstantsV1.WbfsDiscHCopySz);
                }

                if (sectors != null)
                {
                    Byte[] secs = new Byte[sizeof(ushort) * this.wbfsSectorsPerDisc];

                    for (int i = 0; i < sectors.Length; i++)
                        Hton.WriteUInt16(secs, 2 * i, sectors[i]);

                    this.buffer.CopyFrom(secs, 0, WbfsConstantsV1.WbfsDiscHCopySz, secs.Length);
                }

                this.stream.Write(this.buffer.DataPtr, 0, this.hdSectorSize +
                        this.discDescriptorSize * discIdx, this.discDescriptorSize);

                CloseDrive();
                semaphore.Release();

                return true;
            }
            catch
            {
                CloseDrive();
                semaphore.Release();

                throw;
            }
        }

        /// <summary>
        /// Allocates a disc slot
        /// </summary>
        /// <param name="filename">Filename</param>
        /// <param name="slot">Out slot</param>
        /// <returns>False if the disc slots are full or the name is repeated</returns>
        internal bool AllocateDiscSlot(String filename, out int slot)
        {
            String name = filename.Substring(7);
            String code = filename.Substring(0, 6);

            semaphore.Wait();

            for (int i = 0; i < this.maxDiscs; i++)
            {
                if (this.discTable[i] == 0)
                {
                    this.discTable[i] = 1;
                    slot = i;

                    FileSystemEntry entry = new FileSystemEntry(slot, name, code);
                    int p = this.fileTable.BinarySearch(entry, entry);
                    if (p >= 0) 
                    {
                        semaphore.Release();
                        return false;
                    }

                    this.fileTable.Insert(~p, entry);

                    semaphore.Release();
                    return true;
                }
            }

            semaphore.Release();
            slot = -1;
            return false;
        }

        /// <summary>
        /// Frees a disc slot
        /// </summary>
        /// <param name="slot">Slot</param>
        internal void FreeDiscSlot(int slot)
        {
            semaphore.Wait();

            this.discTable[slot] = 0;
            for(int i = 0; i < this.fileTable.Count; i++)
            {
                if (this.fileTable[i].DiscIndex == slot)
                    this.fileTable.RemoveAt(i);
            }

            semaphore.Release();
        }

        /// <summary>
        /// Allocates sectors in the partition
        /// </summary>
        /// <param name="numSectors">The number of sectors</param>
        /// <param name="sectors">Out allocated sectors</param>
        /// <returns>False if there isn't enough free sectors</returns>
        internal bool AllocateSectors(int numSectors, out ushort[] sectors)
        {
            semaphore.Wait();

            // Excludes the FS head
            if (this.totalWbfsSectors - this.usedWbfsSectors - 1 < numSectors)
            {
                sectors = null;
                semaphore.Release();
                return false;
            }

            sectors = new ushort[numSectors];
            this.usedWbfsSectors += numSectors;

            int ibyte = 0, ibit = 0, j = 0;
            for (int i = 0; i < this.totalWbfsSectors; i++)
            {
                if (((this.sectorTable[ibyte] >> ibit) & 0x1) == 1)
                {
                    this.sectorTable[ibyte] &= (byte)(~(1 << ibit));
                    sectors[j] = (ushort)(8 * ibyte + ibit + 1);
                    if (++j == numSectors) break;
                }

                if (++ibit == 8)
                {
                    ibit = 0;
                    ibyte++;
                }
            }

            semaphore.Release();
            return true;
        }

        /// <summary>
        /// Frees sectors in the partition
        /// </summary>
        /// <param name="sectors">Allocated sectors</param>
        internal void FreeSectors(ushort[] sectors)
        {
            semaphore.Wait();

            foreach (ushort sector in sectors)
            {
                int ibyte = (sector - 1) / 8;
                int ibit = (sector - 1) % 8;

                if (((this.sectorTable[ibyte] >> ibit) & 0x1) == 0)
                {
                    this.sectorTable[ibyte] |= (byte)(1 << ibit);
                    this.usedWbfsSectors--;
                }
            }

            semaphore.Release();
        }

        /// <summary>
        /// Synchronizes the filesystem header
        /// </summary>
        /// <exception cref="System.Exception">On operation failure</exception>
        internal void Synchronize()
        {
            this.semaphore.Wait();

            try
            {
                OpenDrive();

                this.stream.Read(this.buffer.DataPtr, 
                    0, 0, this.hdSectorSize);

                this.buffer.CopyFrom(this.discTable, 0, WbfsConstantsV1.WbfsHeadDiscTablePos, 
                    this.discTable.Length);

                this.stream.Write(this.buffer.DataPtr,
                    0, 0, this.hdSectorSize);

                Byte[] sectbl = new Byte[this.sectorTableSize];
                for (int i = 0; i < this.sectorTableSize; i += 4)
                {
                    sectbl[i] = this.sectorTable[i + 3];
                    sectbl[i + 3] = this.sectorTable[i];

                    sectbl[i + 1] = this.sectorTable[i + 2];
                    sectbl[i + 2] = this.sectorTable[i + 1];
                }

                this.buffer.CopyFrom(sectbl, 0, 0, this.sectorTableSize);

                this.stream.Write(this.buffer.DataPtr, 0,
                    this.sectorTablePosition, this.sectorTableSize);

                CloseDrive();
                this.semaphore.Release();
            }
            catch
            {
                this.semaphore.Release();
                throw;
            }
        }

        /// <summary>
        /// Calculates the wbfs sector size for the partition in rotations
        /// </summary>
        /// <returns>Wbfs sector size</returns>
        private byte WbfsSectorSizeSz()
        {
            int wiiTotalSectors = this.totalHdSectors >>
                (WbfsConstantsV1.WiiSectorSizeLSh - this.hdSectorSizeSz);

            Byte size_s;
            for (size_s = 6; size_s < 11; size_s++)
            {
                if (wiiTotalSectors < ((1U << 16) * (1 << size_s)))
                    break;
            }

            return (byte)(size_s + WbfsConstantsV1.WiiSectorSizeLSh);
        }

        /// <summary>
        /// Calculate the number of bit rotations of a number
        /// </summary>
        /// <param name="v">Value</param>
        /// <returns>Rotations</returns>
        private byte Lsh(int v)
        {
            byte i = 0;
            while (v > 1)
            {
                v >>= 1;
                i++;
            }

            return i;
        }

        /// <summary>
        /// Reads a string from a buffer
        /// </summary>
        /// <param name="buffer">Input buffer</param>
        /// <param name="offset">Offset in buffer</param>
        /// <param name="max">Maximum number of characters, set -1 to disable</param>
        /// <returns>String</returns>
        private string ReadString(byte[] buffer, int offset, int max)
        {
            StringBuilder builder;
            if (max <= 0) builder = new StringBuilder();
            else builder = new StringBuilder(max);

            while ((max > 0) || (max < 0))
            {
                if (buffer[offset] == 0)
                    break;

                builder.Append((char)buffer[offset]);

                offset++;
                max--;
            }

            return builder.ToString();
        }
    }
}
